package org.dreamwork.jasmine2.engine;

import org.dreamwork.i18n.IResourceManager;
import org.dreamwork.i18n.LocaleUtil;
import org.dreamwork.jasmine2.configure.JasmineConfig;
import org.dreamwork.jasmine2.events.EventException;
import org.dreamwork.jasmine2.i18n.WebResourceManager;
import org.dreamwork.jasmine2.javac.IClasspathBuilder;
import org.dreamwork.jasmine2.javac.JasmineClassLoader;
import org.dreamwork.jasmine2.javac.JasmineCreator2;
import org.dreamwork.jasmine2.javac.JasmineFileInfo;
import org.dreamwork.jasmine2.javac.configuration.ConfigurationParser;
import org.dreamwork.jasmine2.parser.CharsetDetector;
import org.dreamwork.jasmine2.parser.ParseException;
import org.dreamwork.jasmine2.parser.TaglibDirective;
import org.dreamwork.jasmine2.util.ClasspathBuilderFactory;
import org.dreamwork.jasmine2.web.IWebControl;
import org.dreamwork.jasmine2.web.controls.Page;
import org.dreamwork.util.FileInfo;
import org.dreamwork.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.*;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

import static org.dreamwork.jasmine2.configure.JasmineConfig.*;

/**
 * jasmine 引擎。
 * <p/>
 * <p>jasmine 引擎负责整个 jasmine 应用的解析，编译，运行及类/资源加载。
 * <h3>1. jasmine 页面解析</h3>
 * jasmine 页面使用类似于 jsp 的语法，大多数 jsp 语法在 jasmine 页面中都可以使用，但含义有部分变化：
 * <table>
 * <tr valign="top">
 * <td>
 * </tr>
 * </table>
 * <p/>
 * <p/>
 * Created by IntelliJ IDEA.
 * User: seth
 * Date: 2009-1-21
 * Time: 16:54:45
 */
@WebServlet (name = "Jasmine Engine 2.0", urlPatterns = {"*.jasmine", "*.jscx", "*.pagelet"})
public class JasmineEngine extends HttpServlet {
    private static final Logger logger = LoggerFactory.getLogger (JasmineEngine.class);
    private static final Object locker = new Object ();

    public static final String J2EE_TEMP_DIR = "javax.servlet.context.tempdir";
//    public static final String I18N_SUPPORTED = "i18n.supported";

    public static boolean debugMode = true;

    private File srcdir, outdir, basedir;
    private JasmineClassLoader loader;
    // add by seth.yang on 2018.04.04
    // for customer classloader support
    private ClassLoader customClassLoader;
    private final Map<String, Set<String>> cache = new ConcurrentHashMap<> ();

    private String cp, limitExceedPage;
    private boolean i18nSupported = false;
    private Locale defaultLocale;

    private static Set<URL> urls = new HashSet<> ();
    private static URL[] urlArray;

/*
    public static URL[] getClassURLs () {
        if (urlArray == null) {
            List<URL> list = new ArrayList<> ();
            for (URL url : urls) {
                if (url != null && !StringUtil.isEmpty (url.toString ()))
                    list.add (url);
            }

            URL[] a = new URL[list.size ()];
            if (logger.isDebugEnabled ())
                logger.debug (String.format ("a = %s", java.util.Arrays.toString (a)));
            urlArray = list.toArray (a);
            if (logger.isDebugEnabled ())
                logger.debug (String.format ("urlArray = %s",  java.util.Arrays.toString(urlArray)));
        }
        return urlArray;
    }
*/

    public void init () throws ServletException {
        super.init ();

        ServletConfig config = getServletConfig ();
        String s_mode = config.getInitParameter (KEY_DEBUG);
        if (!StringUtil.isEmpty (s_mode))
            debugMode = "true".equalsIgnoreCase (s_mode.trim ());
        else
            debugMode = JasmineConfig.debug;



        try {
            ServletContext context = getServletContext ();
            basedir = (File) context.getAttribute (J2EE_TEMP_DIR);
            srcdir = new File (basedir, "src");
            outdir = new File (basedir, "classes");

            ConfigurationParser.parse ();

            // add by seth.yang on 2012.10.21 for i18n support
            String value = config.getInitParameter (KEY_I18N_RESOURCE);
            if (!StringUtil.isEmpty (value)) {
                i18nSupported = Boolean.valueOf (value);
                if (i18nSupported) {
                    String resourceBase = config.getInitParameter (KEY_I18N_RESOURCE);
                    value = config.getInitParameter ("i18n.default.locale");
                    defaultLocale = LocaleUtil.parseLocale (value);

                    WebResourceManager manager = new WebResourceManager (context, i18n.defaultLocale, resourceBase);
                    manager.initResources ();
                    context.setAttribute (JASMINE_I18N_HOLDER, manager);
                    context.setAttribute (SUPPORTED_LOCALES, manager.getSupportedLocales ());
                    context.setAttribute (DEFAULT_LOCALE, defaultLocale);
                }
            } else if (i18n != null) {
                i18nSupported = true;
                defaultLocale = (Locale) context.getAttribute (DEFAULT_LOCALE);
            }
            // end of add

            // add by seth.yang on 2014-09-05 for max-upload-size support
            String size = config.getInitParameter ("max-upload-size");
            limitExceedPage = config.getInitParameter ("limit-exceed-page");
            int maxSize = -1;
            if (!StringUtil.isEmpty (size)) {
                char[] buff = size.toCharArray ();
                char unit = buff [buff.length - 1];
                if (unit == 'b' || unit == 'B' || (unit <= '9' && unit >= '0')) {
                    maxSize = Integer.parseInt (size);
                } else if (unit == 'k' || unit == 'K') {
                    maxSize = Integer.parseInt (new String (buff, 0, buff.length - 1)) << 10;
                } else if (unit == 'm' || unit == 'M') {
                    maxSize = Integer.parseInt (new String (buff, 0, buff.length - 1)) << 20;
                } else if (unit == 'g' || unit == 'G') {
                    maxSize = Integer.parseInt (new String (buff, 0, buff.length - 1)) << 30;
                } else {
                    throw new IllegalArgumentException ("Invalid max upload size");
                }
            } else if (io != null) {
                maxSize = JasmineConfig.io.maxUploadSize;
                limitExceedPage = JasmineConfig.io.limitExceedPage;
            }
            context.setAttribute (JasmineRequest.KEY_MAX_UPLOAD_SIZE, maxSize);
            // end of add

            // add by seth.yang on 2018.04.04
            // add customer classloader
            customClassLoader = (ClassLoader) context.getAttribute ("org.dreamwork.jasmine2.CUSTOM_CLASSLOADER");
            if (customClassLoader == null) {
                String classloaderName = context.getInitParameter ("classloader");
                if (StringUtil.isEmpty (classloaderName)) {
                    classloaderName = JasmineConfig.classloader;
                }
                if (!StringUtil.isEmpty (classloaderName)) {
                    if (logger.isDebugEnabled ()) {
                        logger.debug ("using customer classloader: " + classloaderName);
                    }
                    ClassLoader loader = Thread.currentThread ().getContextClassLoader ();
                    Class<?> cls = loader.loadClass (classloaderName);
                    if (ClassLoader.class.isAssignableFrom (cls)) {
                        customClassLoader = (ClassLoader) cls.newInstance ();
                    }
                }
            }
            // end of add 2018.04.04
        } catch (Exception ex) {
            logger.error (ex.getMessage (), ex);
            throw new ServletException (ex);
        }
    }

    private HttpContext buildHttpContext (HttpServletRequest req, HttpServletResponse response) throws IOException {
        HttpServletRequest request;
        if (isMultipartContent (req))
            request = new JasmineRequest (req);
        else
            request = req;

        HttpContext context = new HttpContext ();
        context.setApplication (getServletContext ());
        context.setRequest (request);
        context.setResponse (response);
        context.setSession (request.getSession (true));
        return context;
    }

    protected void service (HttpServletRequest req, HttpServletResponse response) throws ServletException, IOException {
        ClassLoader backup = null;
        // if the custom classloader present, use it
        if (customClassLoader != null) {
            backup = Thread.currentThread ().getContextClassLoader ();
            Thread.currentThread ().setContextClassLoader (customClassLoader);
        }

        JasmineFileInfo info = new JasmineFileInfo (req.getServletPath (), srcdir, outdir);
        debugPageInfo (info);

        Page page = null;
        HttpContext context = buildHttpContext (req, response);
        try {
            HttpServletRequest request = context.getRequest ();
            page = getPage (info, request);
            if (page == null) {
                response.sendError (404);
                return;
            }
            context.setPage (page);
            request.setCharacterEncoding (page.getPageCharset ());
            //add by seth.yang on 2012.10.12 for i18n support
            processI18N (context);

            // start page life circle
            /*lifeCircle (page, context);*/

            // setting current running context
            page.setHttpContext (context);
            // init page and it's child controls
            page.init ();

            page.preload ();

            // load saved states
            // process posted back data
            // fire OnLoad event
            page.load ();

            // valid all page's child controls
            if (page.validControls (page)) {
                // dispatch client event
                page.dispatchEvent ();
            }

            String url = page.getTarget ();
            if (url != null) {
                jump (page, request, response, url);
                return;
            }

            page.loadComplete ();

            // create child controls of page
            if (!page.isChildControlsCreated ())
                page.createChildControls ();
            String contentType = page.getContentType ();
            StringWriter sw = null;
            PrintWriter writer;
            if (StringUtil.isEmpty (contentType)) {
                contentType = "text/plain;charset=utf-8";
                response.setContentType (contentType);
                writer = response.getWriter ();
            } else if (contentType.startsWith ("text/html")) {
                request.setCharacterEncoding (page.getPageCharset ());
                response.setContentType (contentType);
                writer = response.getWriter ();
            } else if (contentType.startsWith ("application/pdf")) {
                sw = new StringWriter ();
                writer = new PrintWriter (sw, true);
            } else {
                throw new ServletException ("Unsupported content type.");
            }

            // render page to response
            page.render (writer);
            writer.flush ();

            if (contentType.startsWith ("application/pdf")) {
                writer.flush ();
                if (sw != null) {
                    sw.flush ();
                    new PDFProcessor (basedir).processPDF (page, sw.getBuffer ().toString ());
                }
            }
        } catch (LimitExceedException ex) {
            String referer = null;
            if (!StringUtil.isEmpty (limitExceedPage)) {
                referer = req.getHeader ("Referer");
            }
            if (!StringUtil.isEmpty (referer)) {
                String url = (req.getContextPath () + limitExceedPage).replace ("//", "/");
                HttpSession session = req.getSession ();
                session.setAttribute ("limit.exceed.referer", referer);
                session.setAttribute ("limit.exceed.exception", ex);
                response.sendRedirect (url);
            } else {
                throw new ServletException (ex);
            }
        } catch (Throwable t) {
            t.printStackTrace ();
            if (debugMode) {
                response.setContentType ("text/html;charset=utf-8");
                logger.error (t.getMessage (), t);
                page = new DefaultErrorPage (t);
                try {
                    lifeCircle (page, context);
                    page.render (response.getWriter ());
                    response.getWriter ().flush ();
                } catch (EventException e) {
                    e.printStackTrace ();
                }
            } else {
                throw new ServletException (t);
            }
        } finally {
            // restore the old thread context classloader
            if (backup != null) {
                Thread.currentThread ().setContextClassLoader (backup);
            }

            try {
                // dispose page.
                if (page != null) page.dispose ();
                // add bye seth.yang on 2013-01-08
                // fix memory leak
                HttpContext.threadLocal.remove ();
            } catch (Throwable t) {
                logger.error (t.getMessage (), t);
            }
        }
    }

    private void processI18N (HttpContext context) {
        if (i18nSupported) {
            HttpSession session = context.getSession ();
            Locale current = (Locale) session.getAttribute (IWebControl.LOCALE_KEY);
            HttpServletRequest request = context.getRequest ();
            Page page = context.getPage ();
            String language = request.getParameter ("__lang");
            Locale locale;
            if (!StringUtil.isEmpty (language)) {
                locale = LocaleUtil.parseLocale (language);
                if (!locale.equals (current)) {
                    session.setAttribute (IWebControl.LOCALE_KEY, locale);
                }
            } else {
                locale = defaultLocale;
                if (current == null) {
                    session.setAttribute (IWebControl.LOCALE_KEY, locale);
                }
            }

            IResourceManager manager = (IResourceManager) getServletContext ().getAttribute (JASMINE_I18N_HOLDER);
            page.setLocale (locale);
            page.setDefaultLocale (defaultLocale);
            page.setSupportedLocales (manager.getSupportedLocales ());
        }
    }

    private void lifeCircle (Page page, HttpContext context) throws EventException, IOException, ServletException {
        // setting current running context
        page.setHttpContext (context);
        // init page and it's child controls
        page.init ();

        page.preload ();

        // load saved states
        // process posted back data
        // fire OnLoad event
        page.load ();

        // valid all page's child controls
        if (page.validControls (page)) {
            // dispatch client event
            page.dispatchEvent ();
        }

        page.loadComplete ();

        String url = page.getTarget ();
        HttpServletRequest request = context.getRequest ();
        HttpServletResponse response = context.getResponse ();
        if (url != null) {
            jump (page, request, response, url);
            return;
        }

        // create child controls of page
        if (!page.isChildControlsCreated ())
            page.createChildControls ();
    }

    private void jump (Page page, HttpServletRequest request, HttpServletResponse response, String url) throws ServletException, IOException {
        int type = page.getGotoType ();
        if (type == 1) {
            RequestDispatcher dispatcher = request.getRequestDispatcher (url);
            ServletRequest r;
            if (request instanceof JasmineRequest)
                r = ((JasmineRequest) request).getOrigRequest ();
            else
                r = request;
            dispatcher.forward (r, response);
        } else
            response.sendRedirect (url);
    }

    private String getAbsolutePath (String relative) {
        if (relative.indexOf ('\\') != -1) relative = relative.replace ('\\', '/');
        String[] a = relative.split ("/");
        Stack<String> stack = new Stack<> ();
        for (String part : a) {
            if ("..".equals (part) && !stack.isEmpty ()) stack.pop ();
            else if (!".".equals (part)) stack.push (part);
        }
        StringBuilder builder = new StringBuilder ();
        for (String part : stack) {
            if (builder.length () > 0) builder.append ('/');
            if (!StringUtil.isEmpty (part)) builder.append (part);
        }
        return builder.toString ();
    }

    private Page getPage (JasmineFileInfo info, HttpServletRequest request) throws IOException, ParseException, ClassNotFoundException, InstantiationException, IllegalAccessException {
        prepareDirs ();
        PageScaner scaner = new PageScaner (request.getServletPath ());
        List<TaglibDirective> list;

        try {
            list = scaner.scan ();
        } catch (FileNotFoundException ex) {
//            response.sendError (HttpServletResponse.SC_NOT_FOUND);
            return null;
        }

        String currentPath = request.getServletPath ();
        int pos = currentPath.lastIndexOf ("/");
        if (pos != -1) currentPath = currentPath.substring (0, pos);

        Map<String, String> map = new HashMap<> ();
        if (list != null) for (TaglibDirective taglib : list) {
            String userControlFile;
            String uri = taglib.getURI ();
            if (uri.charAt (0) == '~')
                userControlFile = uri.substring (1);
            else if (uri.charAt (0) == '/')
                userControlFile = uri;
            else
                userControlFile = getAbsolutePath (currentPath + '/' + taglib.getURI ());
            logger.debug ("find user control: " + userControlFile);
            JasmineFileInfo fileInfo = new JasmineFileInfo (userControlFile, srcdir, outdir);
            synchronized (cache) {
                Set<String> set = cache.get (fileInfo.getClassName ());
                if (set == null) {
                    set = new HashSet<> ();
                    cache.put (fileInfo.getClassName (), set);
                }
                set.add (info.getClassName ());
            }
            fileInfo.createPackageDir ();
            if (needCreatePage (fileInfo)) {
                synchronized (cache) {
                    Set<String> set = cache.get (fileInfo.getClassName ());
                    if (set != null) for (String name : set) {
                        loader.removeLoader (name);
                    }
                }
                logger.debug ("preparing to create user control " + userControlFile);
                createPage (fileInfo, null);
                logger.debug (userControlFile + " created as " + fileInfo.getClassFile ());
            }
            map.put (taglib.getName (), fileInfo.getClassName ());
        }

        if (needCreatePage (info)) {
            loader.removeLoader (info.getClassName ());
            createPage (info, map.size () == 0 ? null : map);
        }

        Page page = loader.load (info.getClassName ());
        if (scaner.isLayoutSupported () && !StringUtil.isEmpty (page.getLayout ())) {
            Page template = loadTemplatePage (page.getLayout (), request);
            if (template.getReplaceHolder () != null) {
                template.getReplaceHolder ().addControl (page);
            }
            page = template;
        }
        return page;
    }

    private Page loadTemplatePage (String uri, HttpServletRequest request) throws ClassNotFoundException, InstantiationException, IllegalAccessException, ParseException, IOException {
        if (uri.charAt (0) == '~')
            uri = request.getContextPath () + uri.substring (1);
        else if (uri.charAt (0) != '/') {
            String base = FileInfo.getFolder (request.getServletPath ());
            uri = FileInfo.getAbsolutePath (base, uri);
        }
        JasmineFileInfo fileInfo = new JasmineFileInfo (uri, srcdir, outdir);
        return getPage (fileInfo, request);
    }

    private boolean needCreatePage (JasmineFileInfo info) {
        ServletContext context = getServletContext ();
        String root = context.getRealPath ("/");
        if (!info.getClassFile ().exists ()) return true;
        else if (root != null) {
            File jsp = new File (context.getRealPath (info.getOrignalPath ()));
            if (jsp.lastModified () > info.getClassFile ().lastModified ())
                return true;
        }
        return false;
    }

    private void createPage (JasmineFileInfo info, Map<String, String> userControls) throws IOException, ParseException {
        String charset;
        Reader in = null;
        InputStream is = null;
        ServletContext context = getServletContext ();

        try {
            String root = context.getRealPath ("/");
            if (root == null) { // war
                URL url = getClass ().getResource (info.getOrignalPath ());
                URLConnection conn = url.openConnection ();
                charset = new CharsetDetector (url).getCharset ();
                is = conn.getInputStream ();
                in = new InputStreamReader (is, charset);
            } else {
                File jsp = new File (context.getRealPath (info.getOrignalPath ()));
                charset = new CharsetDetector (jsp.toURI ().toURL ()).getCharset ();
                is = new FileInputStream (jsp);
                in = new InputStreamReader (is, charset);
            }

            JasmineCreator2 creator = new JasmineCreator2 ();
            creator.setCompileClassPath (getClassPath ());
            if (userControls != null) {
                creator.setUserControl (userControls);
                creator.setUserControlClassPath (info.getClassDir ());
            }
            creator.setReader (in);
            creator.create (info);
        } finally {
            if (is != null) {
                is.close ();
            }
            if (in != null) {
                in.close ();
            }
        }
    }

    private void prepareDirs () throws IOException {
        synchronized (locker) {
            if (!srcdir.exists () && !srcdir.mkdirs ())
                throw new IOException ("Can't create folder: " + srcdir.getCanonicalPath ());
            if (!outdir.exists () && !outdir.mkdirs ())
                throw new IOException ("Can't create folder: " + outdir.getCanonicalPath ());
            if (loader == null) loader = new JasmineClassLoader (outdir);
        }
    }

    private String getClassPath () throws IOException {
        if (cp != null) return cp;
        IClasspathBuilder builder = ClasspathBuilderFactory.newClasspathBuilder (getServletContext ());
        urls = builder.getClasspathUrls ();
        urls.add (outdir.toURI().toURL ());
        JasmineCreator2.setCompileUrls (urls);
        StringBuilder sb = new StringBuilder (builder.getClasspath ());
        sb.append (File.pathSeparatorChar).append (outdir.getCanonicalPath ());
        return cp = sb.toString ();
    }

    private boolean isMultipartContent (HttpServletRequest request) {
        String contentType = request.getHeader ("content-type");
        return contentType != null && contentType.contains ("multipart/form-data");
    }

    private void debugPageInfo (JasmineFileInfo info) {
        if (logger.isDebugEnabled ()) {
            logger.debug ("================= Jasmine Page Info ====================");
            logger.debug ("* class name  : " + info.getClassName ());
            logger.debug ("* file name   : " + info.getFileName ());
            logger.debug ("* orignal path: " + info.getOrignalPath ());
            logger.debug ("* package name: " + info.getPackageName ());
            logger.debug ("* package path: " + info.getPackagePath ());
            logger.debug ("* src file    : " + info.getSrcFile ().getAbsolutePath ());
            logger.debug ("* class file  : " + info.getClassFile ());
            logger.debug ("* class dir   : " + info.getClassDir ());
            logger.debug ("=========================================================");
        }
    }
}